<?php
final class XClassLoader{
	private static $loaders = array();
	private static $singletons = array();
	private static $sessions = array();
	private $class = null;
	private $reflection = null;
	private $autowireConstructParameters = array();
	private $autowireMethods = array();
	private $postConstructMethods = array();
	private $preDestroyMethods = array();
	private $requiredSetters = array();
	private $autowireProperties = array();
	private $requiredProperties = array();
	private $inited = false;
	
	private function __construct($class){	
		if(!class_exists($class, false)
			&& !interface_exists($class, false)){
			throw new XClassNotFoundException("class {$class} does not exist");
		}
		$this->class = $class;
	}
	
	/** 
	 * @return XClassLoader
	 */
	public static function loader($class){
		if(is_object($class)){
			return self::loader(get_class($class));
		}
		if (!isset(self::$loaders[$class])) {
			self::$loaders[$class] = new self($class);
		}
		return self::$loaders[$class];
	}
	
	public function getSingletonInstance(XClassFactory $factory, $class){
		if (!isset(self::$singletons[$class])) {
			$loader = self::loader($class);
			self::$singletons[$class] = $loader->getInstance($factory);
		}
		return self::$singletons[$class];
	}
	
	public function name(){
		return $this->class;
	}
	
	/** 
	 * @return ReflectionClass
	 */
	public function reflection(){
		if(is_null($this->reflection)){
			$this->reflection = new ReflectionClass($this->class);
		}
		return $this->reflection;
	}
	
	/** 
	 * @return object
	 */		
	public function getInstance(XClassFactory $factory, $scope = null, $args = array()){
		if($scope == 'singleton'
			&& isset(self::$singletons[$this->class])){
			return self::$singletons[$this->class];
		}
		$params = $args;
		$constructor = $this->reflection()->getConstructor();
        //autowired constructor
        if(!is_null($constructor)
        	&& count($params) == 0
        	&& is_array($this->autowireConstructParameters)
        	&& count($this->autowireConstructParameters) > 0){
        	$params = array();
			$index = 0;
        	$parameters = $constructor->getParameters();
			foreach($parameters as $parameter){
	        	$name = $parameter->getName();
	        	if(!isset($this->autowireConstructParameters[$name])){
	        		continue;
	        	}
	        	if(isset($this->autowireConstructParameters[$name]['class'])){
	        		$paramScope = x_array_get($this->autowireConstructParameters[$name], 'scope');
					$params[$index++] = $factory->get($this->autowireConstructParameters[$name]['class'], $paramScope);
				}elseif(isset($this->autowireConstructParameters[$name]['value'])){
					$params[$index++] = $this->autowireConstructParameters[$name]['value'];
				}
        	}        	
        }
        $object = $this->_getInstanceByArgs($params);	
        //post-constructor
		foreach($this->postConstructMethods as $postConstructMethod){
			if($this->hasMethod($postConstructMethod)){				
				$object->$postConstructMethod();
			}
		}
		//autowired methods
		foreach($this->autowireMethods as $method => $params){
			if(!$this->hasMethod($method)
				|| !$this->isSetter($method)){
				continue;
			}
			if(isset($params['class'])){
				$object->$method($this->_getInstanceByScope($factory, $params['class'], $params));
			}elseif(isset($params['value'])){
				$object->$method($params['value']);
			}
		}
		//autowired properties
		if($this->isSubclassOf('XObject')){
			foreach($this->autowireProperties as $property => $params){
				if(!$this->hasProperty($property)){
					continue;
				}
				if(isset($params['class'])){
					$object->$property = $this->_getInstanceByScope($factory, $params['class'], $params);
				}elseif(isset($params['value'])){
					$object->$property = $params['value'];
				}
			}
		}elseif(method_exists('ReflectionProperty', 'setAccessible')){
			$reflection = new ReflectionObject($object);
			foreach($this->autowireProperties as $property => $params){
				$prop = $reflection->getProperty($property);
				if(!$this->hasProperty($property)){
					continue;
				}
				$prop->setAccessible(true);				
				if(isset($params['class'])){
					$prop->setValue($property, $this->_getInstanceByScope($factory, $params['class'], $params));
				}elseif(isset($params['value'])){	
					$prop->setValue($property, $params['value']);
				}
			}			
		}else{
			$reflection = new ReflectionObject($object);
			foreach($this->autowireProperties as $property => $params){				
				if(!$this->hasProperty($property)
					|| !$reflection->getProperty($property)->isPublic()){
					continue;
				}
				if(isset($params['class'])){
					$object->$property = $this->_getInstanceByScope($factory, $params['class'], $params);
				}elseif(isset($params['value'])){
					$object->$property = $params['value'];
				}
			}		
		}
		//pre-destroy
		if(count($this->preDestroyMethods) > 0){
			foreach($this->preDestroyMethods as $preDestroyMethod){
				if($this->hasMethod($preDestroyMethod)){
					register_shutdown_function(array($this->class, $preDestroyMethod));
				}
			}
		}
		$this->validate($object);
		if($scope == 'singleton'){
			self::$singletons[$this->class] = $object;
		}
		return $object;
	}
	
	private function _getInstanceByArgs($params = array()){
        $object = null;
		switch(count($params)){
        	case 0:
        		$object = new $this->class();
        		break;
        	case 1:
        		$object = new $this->class($params[0]);
        		break;
        	case 2:
        		$object = new $this->class($params[0], $params[1]);
        		break;
        	case 3:
        		$object = new $this->class($params[0], $params[1], $params[2]);
        		break;
        	default:      			
        		$object = $this->reflection()->newInstanceArgs($params);		
        }
        return $object;	
	}
	
	private function _getInstanceByScope(XClassFactory $factory, $class, $params){
		if(isset($params['scope']) && $params['scope']=='singleton'){
			return $this->getSingletonInstance($factory, $class);
		}
		return $factory->get($class);		
	}
	
	/** 
	 * @return XClassLoader
	 */	
	public function setAutowireConstructParameters($autowireConstructParameters){
		$this->autowireConstructParameters = $autowireConstructParameters;
		return $this;
	}
	
	public function autowireConstructParameters(){
		return $this->autowireConstructParameters;
	}
	
	/** 
	 * @return XClassLoader
	 */	
	public function addAutowireMethod($method, $params){
		$this->autowireMethods[$method] = $params;
		return $this;
	}
	
	public function autowireMethods(){
		return $this->autowireMethods;
	}
	
	/** 
	 * @return XClassLoader
	 */	
	public function addAutowireProperty($property, $params){
		$this->autowireProperties[$property] = $params;
		return $this;
	}
	
	public function autowireProperties(){
		return $this->autowireProperties;
	}
	
	/** 
	 * @return XClassLoader
	 */	
	public function addPostConstructMethod($method){
		$this->postConstructMethods[$method] = $method;
		return $this;
	}
	
	public function postConstructMethods(){
		return $this->postConstructMethods;
	}
	
	/** 
	 * @return XClassLoader
	 */	
	public function addPreDestroyMethod($method){
		$this->preDestroyMethods[$method] = $method;
		return $this;
	}
	
	public function preDestroyMethods(){
		return $this->preDestroyMethods;
	}
	
	/** 
	 * @return XClassLoader
	 */
	public function addRequiredSetter($method){
		$this->requiredSetters[$method] = $method;
		return $this;
	}
	
	public function requiredSetters(){
		return $this->requiredSetters;
	}
	
	/** 
	 * @return XClassLoader
	 */
	public function addRequiredProperties($property){
		$this->requiredProperties[$property] = $property;
		return $this;
	}
	
	public function requiredProperties(){
		return $this->requiredProperties;
	}	
	
	public function isSubclassOf($class){
		return $this->reflection()->isSubclassOf($class);
	}
	
	public function hasAncestor($class){
		return $this->class == $class || $this->isSubclassOf($class);
	}
	
	public function implementsInterface($interface){
		return $this->reflection()->implementsInterface($interface);
	}
	
	public function hasMethod($method){
		return $this->reflection()->hasMethod($method);
	}
	
	public function hasProperty($property){
		return $this->reflection()->hasProperty($property);
	}		
	
	public function isSetter($method){
		if(!preg_match("/^set[A-Z]/", $method)){
			return false;
		}
		$property = strtolower(substr($method, 3, 1)).substr($method, 4);
		if($this->reflection()->hasProperty($property)){
			return true;
		}
		return false;
	}
	
	public function isGetter($method){
		if(!preg_match("/^get[A-Z]/", $method)){
			return false;
		}
		$property = strtolower(substr($method, 3, 1)).substr($method, 4);
		if($this->reflection()->hasProperty($property)){
			return true;
		}
		return false;	
	}
	
	public function inited(){
		return $this->inited;
	}
	
	public function invokeArgs($object, $method, $args = array()){
		if(!$this->hasMethod($method)){
			throw new XMethodNotFoundException("method {$method} not exists in class {$this->class}");
		}
		$function = $this->reflection()->getMethod($method);
		return $function->invokeArgs($object, $args);
	}
	
	/** 
	 * @return XClassLoader
	 */	
	public function setInited($inited){
		$this->inited = $inited;
		return $this;
	}
	
	public function validate($instance){
		foreach($this->requiredSetters as $requiredSetter){
			$getter = $requiredSetter;
			$getter{0} = 'g';
			$property = strtolower(substr($requiredSetter, 3, 1)).substr($requiredSetter, 4);
			$this->_validateProperty($getter, $property, $instance);
		}
		foreach($this->requiredProperties as $requiredProperty){
			$getter = 'get'.ucfirst($requiredProperty);
			$this->_validateProperty($getter, $requiredProperty, $instance);
		}
	}
	
	public function _validateProperty($getter, $property, $instance){
		if($this->hasMethod($getter)){
			$value = $instance->$getter();
			if(!is_null($value)){
				return true;
			}
		}
		if($this->isSubclassOf('XObject')){
			$value = $instance->$property;
			if(!is_null($value)){
				return true;
			}
		}
		if($this->hasMethod('get')){
			$value = $instance->get($property);
			if(!is_null($value)){
				return true;
			}
		}
		throw new XClassLoadException("instance of {$this->class} must have valid property: {$property}");		
	}
			
}
?>